package org.aceor.mddal.group.selector;

import com.alibaba.druid.pool.ExceptionSorter;
import com.alibaba.druid.pool.vendor.MySqlExceptionSorter;
import org.aceor.mddal.common.constants.Constants;
import org.aceor.mddal.group.DataSourceWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author linxuan
 * @author yangzhu
 */
public abstract class AbstractDBSelector implements DBSelector {

    private static final Logger logger = LoggerFactory
            .getLogger(AbstractDBSelector.class);


    /**
     * 判断是否是数据库不可用异常
     */
    protected ExceptionSorter exceptionSorter = new MySqlExceptionSorter();

    protected boolean readable = false;

    public void setReadable(boolean readable) {
        this.readable = readable;
    }

    protected boolean isSupportRetry = true; // 默认情况下支持重试

    public boolean isSupportRetry() {
        return isSupportRetry;
    }

    public void setSupportRetry(boolean isSupportRetry) {
        this.isSupportRetry = isSupportRetry;
    }

    public AbstractDBSelector() {
    }

    protected static class DataSourceHolder {
        public final DataSourceWrapper dsw;
        public final ReentrantLock lock = new ReentrantLock();
        public volatile boolean isNotAvailable = false;
        public volatile long lastRetryTime = 0;

        public DataSourceHolder(DataSourceWrapper dsw) {
            this.dsw = dsw;
        }
    }

    /**
     * 在一个数据库上执行，有单线程试读
     *
     * @param <T>
     * @param dsHolder
     * @param failedDataSources
     * @param tryer
     * @param times
     * @param args
     * @return
     * @throws SQLException
     */
    protected <T> T tryOnDataSourceHolder(DataSourceHolder dsHolder,
                                          Map<DataSource, SQLException> failedDataSources,
                                          DataSourceTryer<T> tryer, int times, Object... args)
            throws SQLException {
        List<SQLException> exceptions = new LinkedList<SQLException>();
        if (failedDataSources != null) {
            exceptions.addAll(failedDataSources.values());
        }
        if (failedDataSources != null
                && failedDataSources.containsKey(dsHolder.dsw)) {
            return tryer.onSQLException(exceptions, args);
        }

        try {
            if (dsHolder.isNotAvailable) {
                boolean toTry = System.currentTimeMillis()
                        - dsHolder.lastRetryTime > Constants.DEFAULT_RETRY_BAD_DS_INTERVAL;
                if (toTry && dsHolder.lock.tryLock()) {
                    try {
                        T t = tryer.tryOnDataSource(dsHolder.dsw, args); // 同一个时间只会有一个线程继续使用这个数据源。
                        dsHolder.isNotAvailable = false; // 用一个线程重试，执行成功则标记为可用，自动恢复
                        return t;
                    } finally {
                        dsHolder.lastRetryTime = System.currentTimeMillis();
                        dsHolder.lock.unlock();
                    }
                } else {
                    exceptions.add(new NoMoreDataSourceException("dsKey:"
                            + dsHolder.dsw.getDataSourceKey()
                            + " not Available,toTry:" + toTry));
                    return tryer.onSQLException(exceptions, args);
                }
            } else {
                return tryer.tryOnDataSource(dsHolder.dsw, args); // 有一次成功直接返回
            }
        } catch (SQLException e) {
            if (exceptionSorter.isExceptionFatal(e)) {
                dsHolder.isNotAvailable = true;
            }
            exceptions.add(e);
            return tryer.onSQLException(exceptions, args);
        }
    }


    public <T> T tryExecute(Map<DataSource, SQLException> failedDataSources,
                            DataSourceTryer<T> tryer, int times, Object... args)
            throws SQLException {
        return tryExecuteInternal(failedDataSources, tryer, times, args);
    }

    public <T> T tryExecute(DataSourceTryer<T> tryer, int times, Object... args)
            throws SQLException {
        return this.tryExecute(new LinkedHashMap<DataSource, SQLException>(0),
                tryer, times, args);
    }

    protected <T> T tryExecuteInternal(DataSourceTryer<T> tryer, int times,
                                       Object... args) throws SQLException {
        return this.tryExecuteInternal(
                new LinkedHashMap<DataSource, SQLException>(0), tryer, times,
                args);
    }

    protected abstract <T> T tryExecuteInternal(
            Map<DataSource, SQLException> failedDataSources,
            DataSourceTryer<T> tryer, int times, Object... args)
            throws SQLException;
}
